/*
 * Copyright (C) 2004-2006 the xine-project
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 *
 * $Id: post.c,v 1.45 2006/04/09 14:27:29 dsalt Exp $
 *
 * post-plugin handling
 */

#ifdef GTK_DISABLE_DEPRECATED
/* Use GtkOptionMenu because tooltips can be added to its items */
# undef GTK_DISABLE_DEPRECATED
#endif

#include "globals.h"

#include <xine.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#include <pango/pango-context.h>

#include "post.h"
#include "gtkvideo.h"
#include "preferences.h"
#include "ui.h"
#include "utils.h"

typedef enum {
  POST_CHAIN_DEINTERLACE,
  POST_CHAIN_VIDEO,
  POST_CHAIN_AUDIO,
  POST_CHAIN_LAST,
} post_chain_t;

typedef struct {
  const post_chain_t type;
  GtkWidget *dbox;
  GSList *plugins; /* list of gxine_post_t */
  const char **names;
} gxine_chain_t;

struct gxine_post_s {
  gxine_chain_t *parent;
  char *name, *parameters;	/* name, text config */
  xine_post_t *plugin;
  xine_post_in_t *info;
  GtkWidget *box, *enabled, *pref, *up;	/* in parent dbox */
  GtkWidget *dbox, **conf_w;	/* this plugin's dbox */
  guint pref_toggled;		/* signal id */
  char *params;			/* raw config */
  void (*reconfigure)(struct gxine_post_s *);
};
typedef struct gxine_post_s gxine_post_t;

static gxine_chain_t chains[] = {
  { POST_CHAIN_DEINTERLACE }, { POST_CHAIN_VIDEO }, { POST_CHAIN_AUDIO }
};

static const char *const cfgname[] = {
  "gui.post_plugins.deinterlace",
  "gui.post_plugins.video",
  "gui.post_plugins.audio"
};

static const void *const empty[] = { NULL };

static const char *const chain_title[] = {
  N_("Configure deinterlace post-plugins"),
  N_("Configure video post-plugins"),
  N_("Configure audio post-plugins")
};

static GtkTooltips *tips = NULL;

static xine_video_port_t *vo_none = NULL;
static xine_audio_port_t *ao_none = NULL;

extern const char *xine_get_post_plugin_description () __attribute__((weak));

static char *
post_parse_parameters (xine_post_t *plugin, const char *paramstr)
{
  const xine_post_in_t *in = xine_post_input (plugin, "parameters");
  const xine_post_api_t *api;
  const xine_post_api_descr_t *param_desc;

  if (!in)
    return NULL;

  api = in->data;
  param_desc = api->get_param_descr ();

  char *params = malloc (param_desc->struct_size);
  api->get_parameters (plugin, params);

  for (;;)
  {
    const char *arg;
    const char *next = strchr (paramstr, ',');

    if (!next)
      next = paramstr + strlen (paramstr);

    while (isspace (*paramstr))
      ++paramstr;
    if (!*paramstr)
      break;

    arg = strchr (paramstr, '=');
    if (arg && ++arg < next)
    {
      const xine_post_api_parameter_t *param = param_desc->parameter - 1;
      while ((++param)->type != POST_PARAM_TYPE_LAST)
      {
	if (strncasecmp (paramstr, param->name, arg - paramstr - 1) ||
	    param->name[arg - paramstr - 1] || param->readonly)
	  continue;

	switch (param->type)
	{
	case POST_PARAM_TYPE_INT:
	  if (param->enum_values)
	  {
	    const char *const *values = param->enum_values;
	    int i = -1;
	    while (values[++i])
	    {
	      if (strncasecmp (values[i], arg, next - arg) ||
		  values[i][next - arg])
		continue;
	      *(int *) (params + param->offset) = i;
	      break;
	    }
	    if (!values[i])
	      *(int *) (params + param->offset) = atoi (arg);
	  }
	  else
	    *(int *) (params + param->offset) = atoi (arg);
	  logprintf ("post: setting %s to %d\n", param->name,
		     *(int *) (params + param->offset));
	  goto next_param;

	case POST_PARAM_TYPE_DOUBLE:
	  sscanf (arg, "%lf", (double *) (params + param->offset));
	  logprintf ("post: setting %s to %lf\n", param->name,
		     *(double *) (params + param->offset));
	  goto next_param;

	case POST_PARAM_TYPE_CHAR:
	  memset (params + param->offset, 0, param->size);
	  memcpy (params + param->offset, arg,
		  (next - arg) < param->size ? (next - arg) : param->size);
	  logprintf ("post: setting %s to \"%.*s\"\n", param->name,
		     param->size, params + param->offset);
	  goto next_param;

	case POST_PARAM_TYPE_STRING:
	  g_printerr (_("gxine: post-parameter type %s not yet supported\n"),
		   "STRING");
	  goto next_param;

	case POST_PARAM_TYPE_STRINGLIST:
	  g_printerr (_("gxine: post-parameter type %s not yet supported\n"),
		   "STRINGLIST");
	  goto next_param;

	case POST_PARAM_TYPE_BOOL:
	  *(int *) (params + param->offset) = atoi (arg) ? 1 : 0;
	  logprintf ("post: setting %s to %s\n", param->name,
		     *(int *) (params + param->offset) ? "true" : "false");
	  goto next_param;

	default:
	  g_printerr (_("gxine: post-parameter type %s not yet supported\n"),
		   "???");
	  goto next_param;
	}
      }
      next_param: /* jump here after recognising and processing a parameter */;
    }

    if (!*next)
      break;
    paramstr = next + 1;
  }

  return params;
}

#if 0
static char *
post_validate_parameters (xine_post_t *plugin, const char *paramstr)
{
  const xine_post_in_t *in = xine_post_input (plugin, "parameters");
  const xine_post_api_t *api = in->data;
  const xine_post_api_descr_t *param_desc = api->get_param_descr ();

  for (;;)
  {
    const char *arg;
    const char *next = strchr (paramstr, ',');

    if (!next)
      next = paramstr + strlen (paramstr);

    while (isspace (*paramstr))
      ++paramstr;
    if (!*paramstr)
      break;

    arg = strchr (paramstr, '=');
    if (arg && ++arg < next)
    {
      const xine_post_api_parameter_t *param = param_desc->parameter - 1;
      while ((++param)->type != POST_PARAM_TYPE_LAST)
      {
	if (!strncasecmp (paramstr, param->name, arg - paramstr - 1) &&
	    !param->name[arg - paramstr - 1])
	{
	  if (!param->readonly)
	    break; /* recognised & writeable */
	  return g_strdup_printf (_("parameter %s is read-only"), param->name);
	}
      }
      if (param->type == POST_PARAM_TYPE_LAST)
	return g_strdup_printf (_("parameter %.*s not recognised"),
				arg - paramstr - 1, paramstr);
    }

    if (!*next)
      break;
    paramstr = next + 1;
  }

  return NULL;
}
#endif

void
post_parse_set_parameters (xine_post_t *plugin, const char *paramstr)
{
  const xine_post_in_t *in = xine_post_input (plugin, "parameters");
  char *params = post_parse_parameters (plugin, paramstr);
  if (params)
  {
    ((xine_post_api_t *)in->data)->set_parameters (plugin, params);
    free (params);
  }
}

/* plugin config window */

static char *
post_make_parameter_string (const gxine_post_t *info)
{
  const xine_post_api_parameter_t *param =
    ((xine_post_api_t *)info->info->data)->get_param_descr ()->parameter - 1;
  char *new_paramstr = strdup ("");

  while ((++param)->type != POST_PARAM_TYPE_LAST)
  {
    switch (param->type)
    {
    case POST_PARAM_TYPE_INT:
      if (param->enum_values)
        asreprintf
	  (&new_paramstr, "%s,%s=%s", new_paramstr, param->name,
	   param->enum_values[*(int *) (info->params + param->offset)]);
      else
	asreprintf (&new_paramstr, "%s,%s=%d", new_paramstr, param->name,
		    *(int *) (info->params + param->offset));
      break;

    case POST_PARAM_TYPE_DOUBLE:
      asreprintf (&new_paramstr, "%s,%s=%lf", new_paramstr, param->name,
		  *(double *) (info->params + param->offset));
      break;

    case POST_PARAM_TYPE_CHAR:
      asreprintf (&new_paramstr, "%s,%s=%.*s", new_paramstr, param->name,
		  param->size, info->params + param->offset);
      break;

    case POST_PARAM_TYPE_BOOL:
      asreprintf (&new_paramstr, "%s,%s=%d", new_paramstr, param->name,
		  *(int *) (info->params + param->offset));
      break;

    default:
      continue;
    }
  }
  /* remove the leading comma */
  memmove (new_paramstr, new_paramstr + 1, strlen (new_paramstr));

  return new_paramstr;
}

static void
post_config_post_revert (gxine_post_t *info)
{
  const xine_post_api_t *api = info->info->data;
  const xine_post_api_descr_t *param_desc = api->get_param_descr ();
  const xine_post_api_parameter_t *param = param_desc->parameter;
  char *params;
  int i;

  params = post_parse_parameters (info->plugin, info->parameters);
  memcpy (info->params, params, param_desc->struct_size);
  free (params);

  for (i = 0; info->conf_w[i]; ++i)
  {
    gpointer param_p = info->params + param[i].offset;
    switch (param[i].type)
    {
    case POST_PARAM_TYPE_INT:
      if (param[i].enum_values)
	gtk_combo_box_set_active (GTK_COMBO_BOX(info->conf_w[i]),
				  *(int *)param_p);
      else
	gtk_spin_button_set_value (GTK_SPIN_BUTTON(info->conf_w[i]),
				   *(int *)param_p);
      break;

    case POST_PARAM_TYPE_DOUBLE:
      gtk_range_set_value (GTK_RANGE(info->conf_w[i]),*(double *)param_p);
      break;

    case POST_PARAM_TYPE_CHAR:
      {
	char *contents = alloca (param[i].size + 1);
	sprintf (contents, "%.*s", param[i].size, (char *)param_p);
	gtk_entry_set_text (GTK_ENTRY(info->conf_w[i]), contents);
      }
      break;

    case POST_PARAM_TYPE_BOOL:
      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(info->conf_w[i]),
				    *(char *)param_p);
      break;

    default:;
    }
  }
}

static void
post_config_post_dispose_dbox (gxine_post_t *info)
{
  if (info->dbox)
    gtk_widget_destroy (info->dbox);
  free (info->params);
  free (info->conf_w);
  info->dbox = NULL;
  info->params = NULL;
  info->conf_w = NULL;
}

static gboolean
post_config_post_window (GtkButton *widget, gxine_post_t *info);

static void
toggle_pref_button (const gxine_post_t *info, gboolean state)
{
  g_signal_handler_block (G_OBJECT (info->pref), info->pref_toggled);
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (info->pref), state);
  g_signal_handler_unblock (G_OBJECT (info->pref), info->pref_toggled);
}

static void
post_config_post_response (GtkDialog *dbox, gint response, gxine_post_t *info)
{
  switch (response)
  {
  case GTK_RESPONSE_REJECT: /* undo */
    post_config_post_revert (info);
    break;
  case GTK_RESPONSE_APPLY:
    info->reconfigure (info);
    break;
  case GTK_RESPONSE_OK:
    info->reconfigure (info);
    /* fall through */
  default:
    post_config_post_dispose_dbox (info);
    toggle_pref_button (info, FALSE);
    break;
  }
}

static void
enum_cb (GtkComboBox *widget, int *param)
{
  *param = gtk_combo_box_get_active (widget);
}

static void
spin_cb (GtkSpinButton *widget, int *param)
{
  *param = gtk_spin_button_get_value_as_int (widget);
}

static void
range_cb (GtkAdjustment *widget, double *param)
{
  *param = widget->value;
}

static void
help_realise_cb (GtkWidget *help, gpointer data)
{
  int i;
  for (i = 0; i < 5; ++i)
  {
    gtk_widget_modify_text (help, i, help->style->fg + i);
    gtk_widget_modify_base (help, i, help->style->bg + i);
  }
}

static void
text_insert_cb (GtkEditable *widget, const gchar *text, gint length,
		gint *position, gpointer data)
{
  int i, j;
  gchar *newtext = malloc (length);
  /* we can't allow ';' or ',' as they have special meanings */
  for (i = j = 0; i < length; ++i)
    if (text[i] >= ' ' && text[i] != ';' && text[i] != ',')
      newtext[j++] = text[i];
  g_signal_handlers_block_by_func (widget, text_insert_cb, data);
  gtk_editable_insert_text (widget, newtext, j, position);
  g_signal_handlers_unblock_by_func (widget, text_insert_cb, data);
  g_signal_stop_emission_by_name (widget, "insert-text");
  free (newtext);
}

static gboolean
char_entry_cb (GtkEditable *widget, GdkEventFocus *event,
	       const xine_post_api_parameter_t *param)
{
  const gxine_post_t *info = g_object_get_data (G_OBJECT(widget), "post");
  memset (info->params + param->offset, 0, param->size);
  strncpy (info->params + param->offset, gtk_editable_get_chars (widget, 0, -1),
	   param->size - 1);
  return FALSE;
}

static void
check_box_cb (GtkToggleButton *widget, int *param)
{
  *param = gtk_toggle_button_get_active (widget);
}

static void
post_config_post_reconfigure (gxine_post_t *info)
{
  char *params = post_make_parameter_string (info);
  logprintf ("post_config: plugin configuration\n  was: %s\n  now: %s\n",
	     info->parameters, params);
  free (info->parameters);
  info->parameters = params;
}

static gboolean
post_config_post_window (GtkButton *widget, gxine_post_t *info)
{
  const xine_post_api_t *api;
  const xine_post_api_descr_t *param_desc;
  const xine_post_api_parameter_t *param;
  char *title;
  GtkWidget *table;
  int param_count = 0;

  if (info->dbox)
  {
    toggle_pref_button (info, TRUE);
    gtk_window_present (GTK_WINDOW(info->dbox));
    return JS_FALSE;
  }

  info->info = xine_post_input (info->plugin, "parameters");
  api = info->info->data;
  param_desc = api->get_param_descr ();

  title = g_strdup_printf (_("Configure plugin %s"), info->name);
  info->dbox =
    gtk_dialog_new_with_buttons (title, NULL, 0,
				 GTK_STOCK_UNDO, GTK_RESPONSE_REJECT,
				 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
				 GTK_STOCK_APPLY, GTK_RESPONSE_APPLY,
				 GTK_STOCK_OK, GTK_RESPONSE_OK,
				 NULL);
  g_signal_connect (G_OBJECT(info->dbox), "response",
		    G_CALLBACK(post_config_post_response), info);
  gtk_dialog_set_default_response (GTK_DIALOG (info->dbox), GTK_RESPONSE_OK);
  ui_add_undo_response (info->dbox, NULL);
  free (title);
  table = gtk_table_new (1, 2, FALSE);
  gtk_box_pack_start (GTK_BOX(GTK_DIALOG(info->dbox)->vbox), table,
		      FALSE, TRUE, 0);

  info->params = post_parse_parameters (info->plugin, info->parameters);
  param = param_desc->parameter - 1;

  while ((++param)->type != POST_PARAM_TYPE_LAST)
  {
    GtkWidget *widget;
    info->conf_w = realloc (info->conf_w,
			    (param_count + 2) * sizeof (GtkWidget *));
    gpointer param_p = info->params + param->offset;
    switch (param->type)
    {
    case POST_PARAM_TYPE_INT:
      if (param->enum_values)
      {
	int i;
	widget = gtk_combo_box_new_text ();
	for (i = 0; param->enum_values[i]; ++i)
	  gtk_combo_box_append_text (GTK_COMBO_BOX(widget),
				     param->enum_values[i]);
	gtk_combo_box_set_active (GTK_COMBO_BOX(widget), *(int *)param_p);
	g_signal_connect (G_OBJECT(widget), "changed",
			  G_CALLBACK(enum_cb), param_p);
      }
      else
      {
	GtkObject *adj =
	  gtk_adjustment_new (*(int *)param_p, INT_MIN, INT_MAX, 1, 10, 0);
	widget = gtk_spin_button_new (GTK_ADJUSTMENT(adj), 1, 0);
	gtk_spin_button_set_update_policy (GTK_SPIN_BUTTON(widget),
					   GTK_UPDATE_ALWAYS);
	gtk_spin_button_set_numeric (GTK_SPIN_BUTTON(widget), TRUE);
	g_signal_connect (G_OBJECT(widget), "value-changed",
			  G_CALLBACK(spin_cb), param_p);
      }
      break;

    case POST_PARAM_TYPE_DOUBLE:
      {
	GtkObject *adj;
	gdouble range = param->range_max - param->range_min;
	gboolean floating = range <= 10 || range != floor (range) ||
			    param->range_min != floor (param->range_min);
	gdouble step = pow (10, floor (log10 (range / 25)));
	gdouble page = pow (10, floor (log10 (range / 10)));

	adj = gtk_adjustment_new (*(double *)param_p,
				  param->range_min, param->range_max,
				  step, page, 0);
	widget = gtk_hscale_new (GTK_ADJUSTMENT(adj));
	gtk_widget_set_size_request (widget, 100, -1);
	gtk_scale_set_draw_value (GTK_SCALE(widget), TRUE);
	gtk_scale_set_value_pos (GTK_SCALE(widget), GTK_POS_TOP);
	gtk_scale_set_digits (GTK_SCALE(widget), floating ? 2 : 0); /* hmm... */
	g_signal_connect (G_OBJECT (adj), "value-changed",
			  G_CALLBACK (range_cb), param_p);
      }
      break;

    case POST_PARAM_TYPE_CHAR:
      {
	char *contents = alloca (param->size + 1);
	widget = gtk_entry_new ();
	sprintf (contents, "%.*s", param->size, (char *)param_p);
	gtk_entry_set_text (GTK_ENTRY(widget), contents);
	g_object_set_data (G_OBJECT(widget), "post", info);
	g_object_connect (G_OBJECT(widget),
		"signal::insert-text", G_CALLBACK(text_insert_cb), NULL,
		"signal::focus-out-event", G_CALLBACK(char_entry_cb), (gpointer)param,
		NULL);
      }
      break;

    case POST_PARAM_TYPE_BOOL:
      widget = gtk_check_button_new();
      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(widget),
				    *(char *)param_p);
      g_signal_connect (G_OBJECT(widget), "toggled",
			G_CALLBACK(check_box_cb), param_p);
      break;

    default:
      continue; /* unknown */
    }

    info->conf_w[param_count] = widget;
    gtk_table_attach (GTK_TABLE(table), widget,
		      1, 2, param_count, param_count + 1, 7, 7, 2, 2);

    if (param->description)
      gtk_tooltips_set_tip (tips, widget, param->description, NULL);

    widget = gtk_label_new (param->name);
    gtk_misc_set_alignment (GTK_MISC (widget), 1, 0.5);
    gtk_table_attach (GTK_TABLE(table), widget,
		      0, 1, param_count, param_count + 1,
		      GTK_FILL, GTK_FILL, 2, 2);

    ++param_count;
  }
  info->conf_w[param_count] = NULL;

  if (api->get_help)
  {
    char *helptext = strdup (api->get_help ());
    size_t p = strlen (helptext);

    /* lose trailing whitespace (and control codes) */
    while (helptext[p] < 33)
      --p;
    helptext[p + 1] = 0;

    {
      GtkWidget *window = gtk_scrolled_window_new (NULL, NULL);
      GtkTextBuffer *buffer = gtk_text_buffer_new (NULL);
      GtkWidget *help = gtk_text_view_new_with_buffer (buffer);
      PangoFontDescription *font;

      gtk_container_add (GTK_CONTAINER(window), help);
      gtk_text_view_set_editable ((GtkTextView *)help, FALSE);

      /* use the normal fg/bg colours, rather than the text display colours */
      g_signal_connect (G_OBJECT(help), "realize",
			G_CALLBACK(help_realise_cb), NULL);

      /* use normal font or, if there are any tabs or no long lines,
       * a monospaced font (it's better to err in this direction);
       * either way, reduce the size a little
       */
      gboolean mono = TRUE, wrap = FALSE;
      char *lf1 = helptext - 1;
      while (lf1)
      {
	char *lf2 = strchr (lf1 + 1, '\n');
	if ((lf2 ? lf2 : lf1 + strlen (lf1)) - lf1 > 82)
	{
	  mono = FALSE;
	  wrap = TRUE;
	  break;
	}
	lf1 = lf2;
      }
      if (strchr (helptext, '\t'))
	mono = TRUE;

      gtk_text_view_set_wrap_mode ((GtkTextView *)help,
				   wrap ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE);

      font = mono ? pango_font_description_from_string ("mono")
		  : pango_font_description_copy_static (help->style->font_desc);

      p = pango_font_description_get_size (help->style->font_desc);
      pango_font_description_set_size (font, p * 5 / 6);
      gtk_widget_modify_font (help, font);
      pango_font_description_free (font);

      g_object_set ((GObject *)help, "indent", 4, "right-margin", 4, NULL);

      gtk_text_buffer_set_text (buffer, helptext, -1);
      gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(window),
				      wrap ? GTK_POLICY_NEVER : GTK_POLICY_AUTOMATIC,
				      GTK_POLICY_AUTOMATIC);

      gtk_widget_set_size_request (window, 1, -1);
      gtk_box_pack_end (GTK_BOX(GTK_DIALOG(info->dbox)->vbox), window,
			TRUE, TRUE, 2);
    }
    free (helptext);

    gtk_box_pack_end (GTK_BOX(GTK_DIALOG(info->dbox)->vbox),
		      gtk_hseparator_new (), FALSE, TRUE, 2);
  }

  hide_on_delete (info->dbox, NULL);
  window_show (info->dbox, info->parent->dbox);

  {
    GdkGeometry geom;
    geom.width_inc = 0;
    geom.height_inc = 1;
    gtk_window_set_geometry_hints (GTK_WINDOW(info->dbox), info->dbox, &geom,
				   GDK_HINT_RESIZE_INC);
  }

  return JS_TRUE;
}

static xine_post_t *
post_config_post_init_plugin (post_chain_t chain_type, const char *name)
{
  logprintf ("post_config: creating plugin '%s'\n", name);
  switch (chain_type)
  {
  case POST_CHAIN_DEINTERLACE:
  case POST_CHAIN_VIDEO:
    return xine_post_init (xine, name, 0, NULL, &vo_none);
  case POST_CHAIN_AUDIO:
    return xine_post_init (xine, name, 0, &ao_none, NULL);
  default:
    return NULL; /* shouldn't happen */
  }
}

/* plugin chain config window */

static void
reconfigure_plugins (gxine_chain_t *chain)
{
  char *params = NULL;
  GSList *info;
  xine_cfg_entry_t entry;

  logprintf ("post_config: configuring plugins\n");

  if (!xine_config_lookup_entry (xine, cfgname[chain->type], &entry))
    return;

  foreach_glist (info, chain->plugins)
  {
    const gxine_post_t *post = info->data;
    gboolean enabled =
      gtk_toggle_button_get_active ((GtkToggleButton *)post->enabled);
    asreprintf (&params, "%s%s%s%s:%s", params ? : "", params ? ";" : "",
		enabled ? "" : "-", post->name, post->parameters);
  }

  entry.str_value = params ? : "";
  preferences_update_entry (&entry);

  free (params);
}

typedef struct { GtkOptionMenu *menu; int index; } reset_pp_t;

static gboolean reset_pp (gpointer data)
{
  reset_pp_t *info = data;
  gtk_option_menu_set_history (info->menu, info->index);
  free (info);
  return FALSE;
}

static void
post_config_post_chain_set_plugin (GtkOptionMenu *menu, gxine_post_t *info)
{
  gxine_chain_t *chain = info->parent;
  const char *name = chain->names[gtk_option_menu_get_history (menu)];
  xine_post_t *newplugin;

  if (!strcmp (name, info->name))
  {
    logprintf ("post_config: replacing plugin '%s' with itself\n", name);
    return;
  }

  if (info->dbox)
    post_config_post_dispose_dbox (info);

  logprintf ("post_config: replacing old plugin '%s'\n", info->name);
  newplugin = post_config_post_init_plugin (chain->type, name);
  if (!newplugin)
  {
    display_error_modal (FROM_XINE, _("Plugin error"),
	/* problem - we can't instantiate the plugin so we can't configure it */
			 _("Couldn't initialise plugin ‘%s’ for configuration"),
			 name);
    if (info->name && strcmp (name, info->name))
    {
      int i;
      for (i = 0; chain->names[i]; ++i)
	if (!strcmp (info->name, chain->names[i]))
	  break;
      if (chain->names[i])
      {
        reset_pp_t *info = malloc (sizeof (reset_pp_t));
        info->menu = menu;
        info->index = i;
	g_idle_add (reset_pp, info);
      }
    }
    return;
  }

  free (info->name);
  free (info->parameters);
  xine_post_dispose (xine, info->plugin);
  info->name = strdup (name);
  info->parameters = strdup ("");
  info->plugin = newplugin;
  gtk_widget_set_sensitive
    (info->pref, xine_post_input (info->plugin, "parameters") != NULL);

  gtk_tooltips_set_tip (tips, (GtkWidget *)menu,
			xine_get_post_plugin_description
			  ? xine_get_post_plugin_description (xine, name)
			  : NULL,
			NULL);
}

static void
post_config_post_chain_delete_plugin (GtkButton *widget, gxine_post_t *info)
{
  logprintf ("post_config: destroying plugin '%s'\n", info->name);
  if (info->dbox)
    g_signal_handlers_disconnect_by_func
      (G_OBJECT(info->dbox), G_CALLBACK(post_config_post_response), info);
  post_config_post_dispose_dbox (info);
  free (info->name);
  free (info->parameters);
  xine_post_dispose (xine, info->plugin);
  gtk_container_remove (GTK_CONTAINER(info->box->parent), info->box);
  info->parent->plugins = g_slist_remove (info->parent->plugins, info);
  free (info);
}

static void
post_config_post_chain_move_plugin (GtkButton *widget, gxine_post_t *info)
{
  gint pos = g_slist_index (info->parent->plugins, info);
  if (--pos < 0)
    return;
  logprintf ("post_config: moving plugin '%s' up\n", info->name);
  /* we're not removing the first item, but keep the compiler happy :-) */
  info->parent->plugins = g_slist_remove (info->parent->plugins, info);
  info->parent->plugins = g_slist_insert (info->parent->plugins, info, pos);
  gtk_box_reorder_child (GTK_BOX(info->box->parent), info->box, pos);
  gtk_widget_set_sensitive (info->up, pos);
  gtk_widget_set_sensitive
    (((gxine_post_t *)g_slist_nth_data (info->parent->plugins, pos + 1))->up,
     TRUE);
}

static void
post_config_post_add_plugin (gxine_chain_t *chain, xine_post_t *post,
			     gboolean enabled, char *name, char *params)
{
  gxine_post_t *info = malloc (sizeof (gxine_post_t));
  GtkWidget *w, *m;
  int i, history = 0;
  const char *hist_desc = NULL;

  logprintf ("post_config: adding plugin '%s'\n", name);
  info->parent = chain;
  info->name = name;
  info->parameters = params;
  info->plugin = post;
  info->info = NULL;
  info->dbox = NULL;
  info->conf_w = NULL;
  info->params = NULL;
  info->reconfigure = post_config_post_reconfigure;
  info->box = gtk_hbox_new (FALSE, 2);

  m = gtk_menu_new ();
  for (i = 0; chain->names[i]; ++i)
  {
    const char *desc = xine_get_post_plugin_description
		     ? xine_get_post_plugin_description (xine, chain->names[i])
		     : NULL;
    GtkWidget *item;
    if (!strcmp (chain->names[i], name))
    {
      history = i;
      hist_desc = desc;
    }
    gtk_menu_shell_append (GTK_MENU_SHELL(m),
			item =   gtk_menu_item_new_with_label (chain->names[i]));
    if (desc)
      gtk_tooltips_set_tip (tips, item, desc, NULL);
  }

  info->enabled = w = gtk_check_button_new ();
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(w), enabled);
  gtk_box_pack_start (GTK_BOX(info->box), w, FALSE, TRUE, 0);

  /* FIXME but not until combo box menu items can have tooltips */
  w = gtk_option_menu_new ();
  gtk_option_menu_set_menu (GTK_OPTION_MENU(w), m);
  gtk_option_menu_set_history (GTK_OPTION_MENU(w), history);
  gtk_box_pack_start_defaults (GTK_BOX(info->box), w);
  g_signal_connect (G_OBJECT(w), "changed",
		    G_CALLBACK(post_config_post_chain_set_plugin), info);
  if (hist_desc)
    gtk_tooltips_set_tip (tips, w, hist_desc, NULL);

  info->up = w = ui_button_new_stock (GTK_STOCK_GO_UP);
  gtk_widget_set_sensitive (w, !!chain->plugins); /* disable for first plugin */
  gtk_box_pack_end (GTK_BOX(info->box), w, FALSE, TRUE, 0);
  g_signal_connect (G_OBJECT(w), "clicked",
		    G_CALLBACK(post_config_post_chain_move_plugin), info);

  w = gtk_button_new_from_stock (GTK_STOCK_REMOVE);
  gtk_box_pack_end (GTK_BOX(info->box), w, FALSE, TRUE, 0);
  g_signal_connect (G_OBJECT(w), "clicked",
		    G_CALLBACK(post_config_post_chain_delete_plugin), info);

  info->pref = w = gtk_toggle_button_new_with_label (GTK_STOCK_PROPERTIES);
  g_object_set (G_OBJECT(w), "use-stock", TRUE, NULL);
  gtk_box_pack_end (GTK_BOX(info->box), w, FALSE, TRUE, 0);
  if (!xine_post_input (post, "parameters"))
    gtk_widget_set_sensitive (w, FALSE);
  else
    info->pref_toggled = g_signal_connect (G_OBJECT(w), "toggled",
					   G_CALLBACK(post_config_post_window),
					   info);

  gtk_box_pack_start (GTK_BOX(GTK_DIALOG(chain->dbox)->vbox), info->box,
		      FALSE, TRUE, 2);
  gtk_widget_show_all (info->box);

  chain->plugins = g_slist_append (chain->plugins, info);
}

static void
post_config_post_chain_add (GtkButton *button, gxine_chain_t *chain)
{
  xine_post_t *plugin =
    post_config_post_init_plugin (chain->type, chain->names[0]);
  if (plugin)
    post_config_post_add_plugin (chain, plugin, TRUE, strdup (chain->names[0]),
				 strdup (""));
  else
    display_error_modal (FROM_XINE, _("Plugin error"),
			 _("Couldn't initialise plugin ‘%s’ for configuration"),
			 chain->names[0]);
}

static void
post_config_post_chain_build (gxine_chain_t *chain)
{
  xine_cfg_entry_t entry;
  char *config;

  if (!xine_config_lookup_entry (xine, cfgname[chain->type], &entry))
    return;

  config = entry.str_value;
  while (config)
  {
    char *first = strchr (config, ':');
    char *next = strchr (config, ';');
    char *name;
    gboolean enabled;
    xine_post_t *post;

    if (!first)
      break;
    if (!next)
      next = (char *)config + strlen (config);

    enabled = *config != '-';
    if (!enabled)
      ++config;
    name = g_strndup (config, first - config);
    post = post_config_post_init_plugin (chain->type, name);
    if (post)
      post_config_post_add_plugin (chain, post, enabled, name,
				   g_strndup (first + 1, next - first - 1));
    else
      free (name);

    if (!*next)
      break;
    config = next + 1;
  }
}

static void
post_config_post_chain_dispose_dbox (gxine_chain_t *chain)
{
  logprintf ("post_config: done\n");
  while (chain->plugins)
    post_config_post_chain_delete_plugin (NULL, chain->plugins->data);
  if (chain->dbox)
    gtk_widget_destroy (chain->dbox);
  free (chain->names);
  chain->names = NULL;
  chain->dbox = NULL;
}

static void
post_config_post_chain_response (GtkDialog *dbox, gint response,
				 gxine_chain_t *chain)
{
  switch (response)
  {
  case GTK_RESPONSE_REJECT: /* undo */
    while (chain->plugins)
      post_config_post_chain_delete_plugin (NULL, chain->plugins->data);
    post_config_post_chain_build (chain);
    break;
  case GTK_RESPONSE_APPLY:
    reconfigure_plugins (chain);
    break;
  case GTK_RESPONSE_CLOSE:
    reconfigure_plugins (chain);
    /* fall through */
  default:
    post_config_post_chain_dispose_dbox (chain);
    break;
  }
}

static JSBool
post_config_post_chain_window (gxine_chain_t *chain)
{
  static const int post_type[] = {
    XINE_POST_TYPE_VIDEO_FILTER,
    XINE_POST_TYPE_VIDEO_FILTER,
    XINE_POST_TYPE_AUDIO_FILTER
  };
  GtkWidget *hbox, *w;
  const char *const *plugin_list;
  char *title;
  int i;

  if (chain->dbox)
  {
    gtk_window_present (GTK_WINDOW(chain->dbox));
    return JS_TRUE;
  }

  plugin_list = xine_list_post_plugins_typed (xine, post_type[chain->type]);
  if (!plugin_list || !plugin_list[0])
  {
    title = g_markup_printf_escaped (_("Plugin chain: %s"),
				     gettext (chain_title[chain->type]));
    display_info (FROM_XINE, title,
		  _("No available plugins - nothing to configure"));
    free (title);
    return JS_FALSE;
  }

  /* clone the plugin list (pointers only); xine-lib may reuse that memory */
  for (i = 0; plugin_list[i]; ++i) /**/;
  chain->names = malloc (i = (i + 1) * sizeof (const char *));
  memcpy (chain->names, plugin_list, i);

  logprintf ("post_config: start\n");

  title = g_strdup_printf (_("Plugin chain: %s"),
			   gettext (chain_title[chain->type]));
  chain->dbox = gtk_dialog_new_with_buttons
		  (title, NULL, 0,
		   GTK_STOCK_UNDO, GTK_RESPONSE_REJECT,
		   GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
		   GTK_STOCK_APPLY, GTK_RESPONSE_APPLY,
		   GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
		   NULL);
  free (title);
  hide_on_delete (chain->dbox, NULL);
  g_signal_connect (G_OBJECT(chain->dbox), "response",
		    G_CALLBACK(post_config_post_chain_response), chain);
  ui_add_undo_response (chain->dbox, NULL);
  if (!tips)
    tips = gtk_tooltips_new ();

  hbox = gtk_hbox_new (FALSE, 2);
  gtk_box_pack_end (GTK_BOX(GTK_DIALOG(chain->dbox)->vbox), hbox,
		    FALSE, TRUE, 5);

  w = gtk_button_new_from_stock (GTK_STOCK_ADD);
  g_signal_connect (G_OBJECT(w), "clicked",
		    G_CALLBACK(post_config_post_chain_add), chain);
  gtk_box_pack_start (GTK_BOX(hbox), w, FALSE, FALSE, 5);

  post_config_post_chain_build (chain);

  window_show (chain->dbox, NULL);
  return JS_TRUE;
}

static JSBool
js_deinterlace_show (JSContext *cx, JSObject *obj, uintN argc,
		     jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("deinterlace_show");
  return post_config_post_chain_window (&chains[POST_CHAIN_DEINTERLACE]);
}

static JSBool
js_pp_video_show (JSContext *cx, JSObject *obj, uintN argc,
		  jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("postproc_video_show");
  return post_config_post_chain_window (&chains[POST_CHAIN_VIDEO]);
}

static JSBool
js_pp_audio_show (JSContext *cx, JSObject *obj, uintN argc,
		  jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("postproc_audio_show");
  return post_config_post_chain_window (&chains[POST_CHAIN_AUDIO]);
}

void
post_init (void)
{
  static const se_f_def_t defs[] = {
    { "deinterlace_show", js_deinterlace_show, 0, 0,
      SE_GROUP_DIALOGUE, NULL, NULL },
    { "postproc_video_show", js_pp_video_show, 0, 0,
      SE_GROUP_DIALOGUE, NULL, NULL },
    { "postproc_audio_show", js_pp_audio_show, 0, 0,
      SE_GROUP_DIALOGUE, NULL, NULL },
    { NULL }
  };
  se_defuns (gse, NULL, defs);

  vo_none = xine_open_video_driver (xine, "none", 0, NULL);
  ao_none = xine_open_audio_driver (xine, "none", NULL);
}
